package com.flexoop.lib.components.arraycollection {

	import mx.collections.ArrayCollection;
	import mx.collections.IList;
	import mx.events.CollectionEvent;
	import mx.events.CollectionEventKind;

	[Bindable]
	public class HistoricalArrayCollection extends ArrayCollection {
		
		private static const UNDO:String	=	"undo";
		private static const REDO:String	=	"redo";
		
		/**
		 * @private
		 * 
		 * prevents more items being added to history if collection is changing due to undo/redo
		 */
		private var _inChange:Boolean = false;
		/**
		 * @private
		 * 
		 * allows storage of what is currently being performed (undo or redo)
		 */
		private var _performing:String = UNDO;
		/**
		 * @private
		 * 
		 * To improve performance, this will limit how many revisions can be made by the user
		 */
		private var _maximumRevisions:uint;
		/**
		 * @private
		 * 
		 * create an index to traverse _history arrayCollection
		 */
		private var _historyIndex:int=-1;
		/**
		 * @private
		 * 
		 * Keeps track of changes to the original arrayCollection
		 */
		private var _history:ArrayCollection = new ArrayCollection;
		/**
		 * @private
		 * 
		 * Storage for old data that is overwritten when setting "list"
		 */
		private var _oldSource:Array = [];
		private var _newSource:Array = [];
		
		public function get history():ArrayCollection {
			return this._history;
		}
		
		public function set history( value:ArrayCollection ):void {
			this._history = value;
		}

		public function HistoricalArrayCollection( source:Array=null, maximumRevisions:uint=30 ) {
			super( source );
			if ( maximumRevisions < 1 ) {
				_maximumRevisions = 1;
			} else {
				_maximumRevisions = maximumRevisions;
			}
			this.addEventListener( CollectionEvent.COLLECTION_CHANGE, handleCollectionChange );
		}
		
		private function handleCollectionChange( event:CollectionEvent ):void {
			// if history is not undergoing some sort of data change
			if ( !_inChange ) {
				// split array and set new history
				history = new ArrayCollection( history.source.slice( 0, _historyIndex + 1 ) );
				// add new event
				history.addItem( event );
				// if history is larger than maximum revisions, remove oldest change
				if ( history && history.length > _maximumRevisions ) {
					history.removeItemAt( 0 );
				}
				// if event is a "RESET", store new data for retrieval later (needed during redo phase)
				if ( event.kind == CollectionEventKind.RESET ) {
					this._newSource = this.list.toArray();
				}
				// set index to last item
				_historyIndex = history.length - 1;
			}
		}
		
		// have to override "set list" as old source data needed for undo phase
		override public function set list( value:IList ):void {
			// make sure the arrayCollection is not undergoing the undo/redo phase
			if ( !_inChange ) {
				this._oldSource = this.source;
			}
			super.list = value;
		}
		
		/**
		 * @param step Number of steps to go back when undoing the ArrayCollection
		 * 
		 * @return Returns an arraycollection of the items removed in the undo steps
		 */
		public function undo( step:uint=1 ):ArrayCollection {
			return makeChange( -step );
		}
		
		/**
		 * @param step Number of steps to go forward when redoing the ArrayCollection
		 * 
		 * @return Returns an arraycollection of the items added in the redo steps
		 * 
		 */
		public function redo( step:uint=1 ):ArrayCollection {
			return makeChange( step );
		}
		
		/**
		 * @private
		 * 
		 * @param step Number of steps to go forward/back during redo/undo
		 * 
		 * @return Returns an arraycollection of the items added/removed during redo/undo
		 * 
		 * Performs the actual changes on the array collection
		 */
		private function makeChange( step:int=1 ):ArrayCollection {
			var _acToReturn:ArrayCollection = new ArrayCollection;
			
			// set state
			_performing = ( step > 0 ) ? REDO : UNDO;
			
			var _direction:int = ( step > 0 ) ? 1 : -1;
			var _index:int = 0; 
			_inChange = true;
			
			while ( _index != step ) {
				// if performing a "redo" need to move pointer to next step first
				if ( _direction > 0 ) {
					_historyIndex++;
				}
				// check whether trying to access something outside of the scope of the history array
				if ( _historyIndex < 0 || _historyIndex == history.length ) {
					// reset _historyIndex before returning;
					if ( _historyIndex == history.length ) {
						_historyIndex = _historyIndex - _direction;
					}
					break;
				}
				
				var _event:CollectionEvent = history.getItemAt( _historyIndex ) as CollectionEvent;

				switch ( _event.kind ) {
					case CollectionEventKind.ADD: handleAdd( _event ); break;
					case CollectionEventKind.REMOVE: handleRemove( _event ); break;
					case CollectionEventKind.MOVE: handleMove( _event ); break;
					case CollectionEventKind.REFRESH: handleRefresh( _event ); break;
					case CollectionEventKind.REPLACE: handleReplace( _event); break;
					case CollectionEventKind.RESET: handleReset( _event ); break;
					case CollectionEventKind.UPDATE: handleUpdate( _event ); break; // needs more research
				}

				// if performing an "undo", move pointer to previous step last
				if ( _direction < 0 ) {
					_historyIndex = _historyIndex + _direction;
				}
				_index = _index + _direction;
			}
			
			_inChange = false;
			return _acToReturn;
		}
		
		/**
		 * @private
		 * 
		 * @param event CollectionEvent to undo or redo
		 * 
		 * Handles a CollectionEventKind of "ADD"
		 */
		private function handleAdd( event:CollectionEvent ):void {
			// event.kind prevents infinite loop when being called from performRemove
			if ( _performing == UNDO && event.kind == CollectionEventKind.ADD ) {
				// call removeData for UNDO
				handleRemove( event );
			} else {
				// reverse array so all items go back in in the same order they came out
				var _arrReversedItems:Array = event.items.reverse();
				for each ( var item:Object in _arrReversedItems ) {
					this.addItemAt( item, event.location );
				}
			}
		}
		
		/**
		 * @private
		 * 
		 * @param event CollectionEvent to undo or redo
		 * 
		 * Handles a CollectionEventKind of "REMOVE"
		 */
		private function handleRemove( event:CollectionEvent ):void {
			// event.kind prevents infinite loop when being called from performAdd
			if ( _performing == UNDO && event.kind == CollectionEventKind.REMOVE ) {
				// call addData for UNDO
				handleAdd( event );
			} else {
				// loop over added items and remove all newly added items
				var _addIndex:int = event.items.length;
				while ( _addIndex > 0 ) {
					this.removeItemAt( event.location );
					_addIndex--;
				}
			}
		}

		/**
		 * @private
		 * 
		 * @param event CollectionEvent to undo or redo
		 * 
		 * Handles a CollectionEventKind of "REPLACE"
		 */
		private function handleReplace( event:CollectionEvent ):void {
			// always 1 item as location is single uint value
			var _newValue:Object = event.items[0].newValue;
			if ( _performing == UNDO ) {
				// undo has to switch new for old
				_newValue = event.items[0].oldValue;
			}
			this.setItemAt( _newValue, event.location );
		}

		/**
		 * @private
		 * 
		 * @param event CollectionEvent to undo or redo
		 * 
		 * Handles a CollectionEventKind of "MOVE"
		 */
		private function handleMove( event:CollectionEvent ):void {
			// always 1 item as location is single uint value
			var _newLocation:uint = event.items[0].location;
			var _oldLocation:uint = event.items[0].oldLocation;
			if ( _performing == UNDO ) {
				// undo has to switch new for old
				_newLocation = event.items[0].oldLocation;
				_oldLocation = event.items[0].location;
			}
			this.addItemAt( this.removeItemAt( _oldLocation ), _newLocation );
		}

		/**
		 * @private
		 * 
		 * @param event CollectionEvent to undo or redo
		 * 
		 * Handles a CollectionEventKind of "REFRESH"
		 */
		private function handleRefresh( event:CollectionEvent ):void {
			var _functionHolder:Function = event.target.filterFunction;
			if ( _performing == UNDO ) {
				this.filterFunction = null;
			} else {
				this.filterFunction = _functionHolder;
			}
			this.refresh();
			// reset filterFunction so it can be reused by "redo" if necessary
			this.filterFunction = _functionHolder;
		}

		/**
		 * @private
		 * 
		 * @param event CollectionEvent to undo or redo
		 * 
		 * Handles a CollectionEventKind of "RESET"
		 */
		private function handleReset( event:CollectionEvent ):void {
			if ( _performing == UNDO ) {
				this._newSource = this.list.toArray();
				this.list = new ArrayCollection( this._oldSource );
			} else {
				this._oldSource = this.list.toArray();
				this.list = new ArrayCollection( this._newSource );
			}
		}

		/**
		 * @private
		 * 
		 * @param event CollectionEvent to undo or redo
		 * 
		 * Handles a CollectionEventKind of "UPDATE"
		 */
		private function handleUpdate( event:CollectionEvent ):void {
			// this one won't work correctly unless it is a non-generic object that passes back
			// when a change has occurred
			/* var tempStuff:String = '';
			if ( _performing == UNDO ) {
				var temp:String = '';
			} else {
				var temp2:String = '';
			} */
		}
	}
}